/*
 * Copyright 2010 Francisco Modesto
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.googlecode.gwt.tinyaop.rebind;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.util.collect.HashSet;
import com.googlecode.gwt.tinyaop.client.WithInterceptor;

public class InterceptorGenerator extends Generator {

  @Override
  public String generate(TreeLogger logger, GeneratorContext context, String typeName)
  throws UnableToCompleteException {
    TypeOracle oracle = context.getTypeOracle();
    check(oracle, logger, typeName, "Kernel panic!");
    JClassType classType = oracle.findType(typeName);
    check(classType, logger, typeName, "Could not find the type.");
    TreeLogger proxyLogger = branchLogger(logger, typeName);

    try {
      Class<? extends Annotation>[] custom = getCustomAnnotations(proxyLogger, classType);
      return selectCreator(classType, custom).create(proxyLogger, context);
    }
    catch (UncheckUnableToCompleteException e) {
      proxyLogger.log(Type.ERROR, e.getMessage());
      throw new UnableToCompleteException();
    }
  }

  private void check(Object object, TreeLogger logger, String type, String msg)
  throws UnableToCompleteException {
    if (object == null) {
      logger.log(Type.ERROR, "Unable to rebind " + type + " - " + msg);
      throw new UnableToCompleteException();
    }
  }

  private TreeLogger branchLogger(TreeLogger logger, String typeName) {
    String msg = "Generating interceptor proxy for '" + typeName + "'";
    return logger.branch(TreeLogger.DEBUG, msg, null);
  }

  public Class<? extends Annotation>[] getCustomAnnotations(TreeLogger logger, JClassType type) {
    try {
      HashSet<Class<? extends Annotation>> annotations = new HashSet<Class<? extends Annotation>>();
      Class<?> c = Class.forName(type.getQualifiedBinaryName());
      for (Method method : getAllMethods(c)) {
        getCustomAnnotations(annotations, method);
      }
      @SuppressWarnings("unchecked")
      Class<? extends Annotation>[] result = annotations.toArray(new Class[annotations.size()]);
      Arrays.sort(result, new Comparator<Class<? extends Annotation>>() {
        @Override
        public int compare(Class<? extends Annotation> o1, Class<? extends Annotation> o2) {
          return priority(o1) - priority(o2);
        }
        
        private int priority(Class<? extends Annotation> annotation) {
          return annotation.getAnnotation(WithInterceptor.class).order();
        }
      });
      return result;
    }
    catch (Throwable t) {
      throw new UncheckUnableToCompleteException("Unable to access custom annotations");
    }
  }

  private Method[] getAllMethods(Class<?> type) {
    HashSet<Class<?>> visited = new HashSet<Class<?>>();
    HashSet<Method> methods = new HashSet<Method>();
    getAllMethods(methods, visited, type);
    return methods.toArray(new Method[methods.size()]);
  }
  
  private void getAllMethods(HashSet<Method> methods, HashSet<Class<?>> visited, Class<?> type) {
    if (visited.contains(type)) {
      return;
    }
    else {
      visited.add(type);
    }
    
    if (type.getSuperclass() != null) {
      getAllMethods(methods, visited, type.getSuperclass());
    }
    for (Class<?> iface : type.getInterfaces()) {
      getAllMethods(methods, visited, iface);
    }
    for (Method method : type.getDeclaredMethods()) {
      methods.add(method);
    }
  }

  private void getCustomAnnotations(Set<Class<? extends Annotation>> annotations, Method method)
  throws Throwable {
    for (Annotation a : method.getAnnotations()) {
      if (a.annotationType().isAnnotationPresent(WithInterceptor.class)) {
        annotations.add(a.annotationType());
      }
    }
  }
  
  private AbstractCreator selectCreator(JClassType classType, Class<? extends Annotation>[] custom) {
    if (isInterface(classType)) {
      return new InterfaceProxyCreator(classType, custom);
    }
    else {
      return new ClassProxyCreator(classType, custom);
    }
  }

  private boolean isInterface(JClassType classType) {
    return classType.isInterface() != null;
  }
}
